import 'package:number_system/number_system.dart';

abstract class DoesIPMatchIPOrSubnetMask {
  bool call({
    required String ip,
    required String ipOrSubnetMask,
  });
}

class DoesIPMatchIPOrSubnetMaskImpl implements DoesIPMatchIPOrSubnetMask {
  const DoesIPMatchIPOrSubnetMaskImpl();

  @override
  bool call({
    required String ip,
    required String ipOrSubnetMask,
  }) {
    final isIPv4 = ip.contains('.');
    final isIPv6 = ip.contains(':');
    final isIPOrSubnetMaskv4 = ipOrSubnetMask.contains('.');
    final isIPOrSubnetMaskv6 = ipOrSubnetMask.contains(':');

    if (isIPv4 && isIPOrSubnetMaskv6 || isIPv6 && isIPOrSubnetMaskv4) {
      return false;
    }

    if (isIPv4) {
      return _doesIPv4MatchIPOrSubnetMask(
        ipv4: ip,
        ipv4OrSubnetMask: ipOrSubnetMask,
      );
    }

    if (isIPv6) {
      return _doesIPv6MatchIPOrSubnetMask(
        ipv6: ip,
        ipv6OrSubnetMask: ipOrSubnetMask,
      );
    }

    throw UnimplementedError();
  }

  bool _doesIPv4MatchIPOrSubnetMask({
    required String ipv4,
    required String ipv4OrSubnetMask,
  }) {
    if (!ipv4OrSubnetMask.contains('/')) {
      return ipv4 == ipv4OrSubnetMask;
    }

    final subnetMask = ipv4OrSubnetMask;
    final subnetMaskParts = subnetMask.split('/');
    final subnetMaskIPPart = subnetMaskParts.first;
    final networkPartBitCount = int.parse(subnetMaskParts.last);

    final ipv4BinaryNumber = _convertIPv4toBinaryNumber(
      ipv4: ipv4,
    );

    final subnetMaskIPPartBinaryNumber = _convertIPv4toBinaryNumber(
      ipv4: subnetMaskIPPart,
    );

    final ipv4NetworkPartBinaryNumber = ipv4BinaryNumber.substring(0, networkPartBitCount);

    final subnetMaskNetworkPartBinaryNumber =
        subnetMaskIPPartBinaryNumber.substring(0, networkPartBitCount);

    return ipv4NetworkPartBinaryNumber == subnetMaskNetworkPartBinaryNumber;
  }

  String _convertIPv4toBinaryNumber({
    required String ipv4,
  }) {
    final ipv4Blocks = ipv4.split('.');

    final stringBuffer = StringBuffer();

    for (var i = 0; i < ipv4Blocks.length; i++) {
      final ipv4Block = ipv4Blocks[i];

      final ipv4BlockBinaryNumber = _addLeadingZeros(
        s: int.parse(ipv4Block).decToBinary(),
        desiredDigitCount: 8,
      );

      stringBuffer.write(ipv4BlockBinaryNumber);
    }

    return stringBuffer.toString();
  }

  String _expandIPv6({
    required String ipv6,
  }) {
    if (!ipv6.contains('::')) {
      return ipv6;
    }

    final ipv6Parts = ipv6.split('::');
    var minifiedIPBlockCount = 8;

    for (var i = 0; i < ipv6Parts.length; i++) {
      final ipv6Part = ipv6Parts[i];

      final ipv6PartBlockCount = ipv6Part.split(':').where((ipv6Block) {
        return ipv6Block.isNotEmpty;
      }).length;

      minifiedIPBlockCount -= ipv6PartBlockCount;
    }

    final stringBuffer = StringBuffer();

    if (ipv6Parts.first.isNotEmpty) {
      stringBuffer.write(ipv6Parts.first);
      stringBuffer.write(':');
    }

    for (var i = 0; i < minifiedIPBlockCount; i++) {
      stringBuffer.write('0');

      if (i != minifiedIPBlockCount - 1) {
        stringBuffer.write(':');
      }
    }

    if (ipv6Parts.last.isNotEmpty) {
      stringBuffer.write(':');
      stringBuffer.write(ipv6Parts.last);
    }

    return stringBuffer.toString();
  }

  String _convertIPv6toBinaryNumber({
    required String ipv6,
  }) {
    final expandedIPv6 = _expandIPv6(
      ipv6: ipv6,
    );

    final ipv6Blocks = expandedIPv6.split(':');

    final stringBuffer = StringBuffer();

    for (var i = 0; i < ipv6Blocks.length; i++) {
      final ipv6Block = ipv6Blocks[i];

      final ipv6BlockBinaryNumber = _addLeadingZeros(
        s: ipv6Block.hexToDEC().decToBinary(),
        desiredDigitCount: 16,
      );

      stringBuffer.write(ipv6BlockBinaryNumber);
    }

    return stringBuffer.toString();
  }

  String _addLeadingZeros({
    required String s,
    required int desiredDigitCount,
  }) {
    final zeroCount = desiredDigitCount - s.length;

    if (zeroCount <= 0) {
      return s;
    }

    final stringBuffer = StringBuffer();

    for (var i = 0; i < zeroCount; i++) {
      stringBuffer.write('0');
    }

    stringBuffer.write(s);

    return stringBuffer.toString();
  }

  bool _doesIPv6MatchIPOrSubnetMask({
    required String ipv6,
    required String ipv6OrSubnetMask,
  }) {
    if (!ipv6OrSubnetMask.contains('/')) {
      final firstExpandedIPv6 = _expandIPv6(
        ipv6: ipv6,
      );
      final secondExpandedIPv6 = _expandIPv6(
        ipv6: ipv6OrSubnetMask,
      );

      return firstExpandedIPv6 == secondExpandedIPv6;
    }

    final ipv6SubnetMask = ipv6OrSubnetMask;
    final ipv6SubnetMaskParts = ipv6SubnetMask.split('/');
    final ipv6SubnetMaskIPPart = ipv6SubnetMaskParts.first;
    final networkPartBitCount = int.parse(ipv6SubnetMaskParts.last);

    final ipv6BinaryNumber = _convertIPv6toBinaryNumber(
      ipv6: ipv6,
    );

    final ipv6SubnetMaskIPPartBinaryNumber = _convertIPv6toBinaryNumber(
      ipv6: ipv6SubnetMaskIPPart,
    );

    final ipv6NetworkPartBinaryNumber = ipv6BinaryNumber.substring(0, networkPartBitCount);

    final ipv6SubnetMaskNetworkPartBinaryNumber =
        ipv6SubnetMaskIPPartBinaryNumber.substring(0, networkPartBitCount);

    return ipv6NetworkPartBinaryNumber == ipv6SubnetMaskNetworkPartBinaryNumber;
  }
}
